Python 面向对象:从类与实例到 MRO
面向 Java 背景学习者,系统梳理 Python 的类、继承、MRO、魔术方法、封装与多态。
INFO 阅读提示 本笔记面向有 Java 背景的学习者,重点标注了 Python 与 Java 的差异。 🐤 = 鸭子类型相关 ⚠️ = 容易踩的坑 🆚 = 与 Java 的区别
目录
- 一、类与实例
- 二、继承
- 三、super() 与 MRO
- 四、魔术方法(Magic Methods)
- 五、封装
- 六、多态
- 七、抽象基类与接口
- 八、方法的三种类型
- 九、Python vs Java 速查表
ABSTRACT 面向对象三大特性 封装(隐藏内部细节)·继承(复用与扩展)·多态(同一接口不同行为)
一、类与实例
类是模板,实例是用模板造出来的具体对象。
class Animal:
kingdom = "Animalia" # 类属性:所有实例共享
def __init__(self, name, age): # 构造函数
self.name = name # 实例属性:每个实例独立
self.age = age
def speak(self):
return f"我叫 {self.name}"
a = Animal("旺财", 3)
a.speak() # 我叫 旺财
Animal.kingdom # Animalia(也可通过类名访问)类属性 vs 实例属性
类属性很像 Java 的静态变量:属于类本身、所有实例共享、可用类名访问。
WARNING ⚠️ 类属性赋值陷阱(和 Java 不一样) 通过实例给类属性赋值,不会修改类属性,而是新建一个同名实例属性把它遮住。
class Dog: count = 0 d1 = Dog() d1.count = 99 # 新建 d1 的实例属性,不是改类属性! print(d1.count) # 99 ← 实例属性 print(Dog.count) # 0 ← 类属性没变 Dog.count = 99 # ✅ 必须通过类名才能改类属性
NOTE 类属性 vs 实例属性,一句话区分 类属性存在类对象里(如
Animal.kingdom),所有实例查找共享;实例属性存在具体对象里(如a.name),每个对象单独一份。a = Animal("旺财", 3) b = Animal("小黑", 5) a.name # 旺财 b.name # 小黑 ← 互不影响,各自一份 # a.__dict__ = {"name": "旺财", "age": 3} # b.__dict__ = {"name": "小黑", "age": 5}这条区别在继承里还会反复用到:类属性是「类之间查找共享」,实例属性是「每个对象单独拥有」。
动态访问属性:getattr / setattr
getattr / setattr 是内置函数(不是类自动生成的),相当于 Java 的反射。
当属性名是运行时才知道的字符串时才需要它们。
field = "name" # 属性名来自变量/输入/配置
getattr(dog, field) # 等价于 dog.name
setattr(dog, field, "小黑") # 等价于 dog.name = "小黑"
getattr(dog, "color", "未知") # 第三个参数:不存在时的默认值TIP 和 eval 的关系 思路类似(把字符串当标识符用),但
getattr是eval的安全子集——只能访问属性,不能执行任意代码。能用getattr就别用eval。
二、继承
class Dog(Animal): # 继承 Animal(Java 的 extends)
def __init__(self, name, age, breed):
super().__init__(name, age) # 调用父类构造函数
self.breed = breed
def speak(self): # 方法重写:覆盖父类同名方法
return f"汪!我是 {self.name}"是否必须写 super().init()
WARNING ⚠️ 子类一旦写了
__init__就必须手动调 super()
情况 结果 子类没有 __init__自动继承父类的,无需手写 子类有 __init__但没调 super()父类属性全部丢失( self.name不存在)🆚 Java 会自动插入
super(),Python 完全靠你自己写。
NOTE 没调 super(),丢的只是「实例属性」 即使子类
__init__没写super().__init__(),父类的方法、类属性、类方法、静态方法照样继承——它们都挂在类上,天然可见。 唯一丢的是父类__init__里self.xxx = ...创建的实例属性:那段代码得靠__init__跑起来才生效,而子类的__init__把父类的覆盖了、没机会执行。class Animal: kind = "动物" # 类属性 → 继承 def eat(self): ... # 方法 → 继承 def __init__(self): self.name = "动物" # 实例属性 → 丢失(除非调 super) class Dog(Animal): def __init__(self): self.color = "黑色" # 覆盖父类 __init__,且没调 super d = Dog() d.eat() # ✅ 方法继承 d.kind # ✅ 类属性继承 → "动物" d.color # ✅ "黑色" d.name # ❌ AttributeError:父类 __init__ 没执行
常用判断函数
isinstance(d, Dog) # 实例是否属于某类(含父类)
issubclass(Dog, Animal) # 类是否是某类的子类类属性的继承:查找而非复制
NOTE 子类不会「复制」父类的类属性 子类读类属性时,是沿 MRO 去父类查找,并没有在自己身上存一份。
class Animal: kind = "动物" class Dog(Animal): pass Dog.kind # "动物"(从父类找到的) "kind" in Dog.__dict__ # False ← 子类自己没有 "kind" in Animal.__dict__ # True ← 真身在父类
WARNING ⚠️ 子类赋值 = 新建覆盖,不改父类 这和[实例赋值陷阱](#类属性 vs 实例属性)是一对孪生坑,只是换成了「子类 vs 父类」:
Dog.kind = "狗" Dog.kind # "狗" Animal.kind # "动物" ← 父类没变 "kind" in Dog.__dict__ # True ← 赋值后子类才有了自己的 kind
Dog.kind = "狗"是在Dog里新建一个kind把父类的遮住,不是修改Animal.kind。
DANGER ⚠️ 可变类属性会「串台」 如果类属性是 list / dict 这类可变对象,子类没赋值时拿到的是父类那同一个对象,原地修改会改到父类(以及所有兄弟子类):
class Animal: nums = [] class Dog(Animal): pass Dog.nums.append(1) # 原地修改 → 改的是父类那个列表 Animal.nums # [1] ← 被连累了 Dog.nums = [1, 2, 3] # 重新赋值 → 子类新建,与父类互不影响 Animal.nums # [] (若上面没 append 过)规律:原地修改(append/update)会穿透到父类,重新赋值只在子类生效。
想从子类改父类的类属性,怎么办
既然 Dog.kind = x 只会创建子类自己的属性,要真正改到父类,用下面三种之一:
# 方式一:直接用父类名(最清楚,推荐)
Animal.kind = "生物"
# 方式二:通过 __bases__ 找到父类(能用但太绕,不推荐)
Dog.__bases__[0].kind = "生物" # __bases__ 是直接父类的元组 (Animal,)
# 方式三:父类提供类方法,内部写死父类名
class Animal:
kind = "动物"
@classmethod
def change_kind(cls, value):
Animal.kind = value # ⚠️ 必须写 Animal,不能写 clsWARNING ⚠️ 方式三里别写
cls.kind = value子类调用时cls是Dog,cls.kind = value会变成给Dog新建属性,又掉进「赋值=覆盖」的陷阱。要改父类就得写死Animal.kind。
三、super() 与 MRO
NOTE super() 不是「父类对象」 它是一个代理对象,指向 MRO 链里当前类的下一个。 单继承时下一个恰好是父类,所以看起来像父类。
什么是 MRO
MRO = Method Resolution Order(方法解析顺序),即调用方法时 Python 按什么顺序去各个类里找。
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__mro__)
# (D, B, C, A, object) A
/ \
B C MRO: D → B → C → A → object
\ /
D多继承:同名方法谁优先
括号里左边优先,但底层由 C3 算法保证三条规则:
- 子类永远排在父类前面
- 括号里的左右顺序保持不变
- 两条规则冲突时直接报
TypeError
class A:
def hello(self): return "A"
class B(A):
def hello(self): return "B"
class C(A):
def hello(self): return "C"
class D(B, C): pass
D().hello() # "B",因为 MRO 里 B 在 C 前面EXAMPLE super() 沿 MRO 逐级调用
class D(B, C): # MRO: D → B → C → A def __init__(self): super().__init__() # B 里的 super() 调的是 C,不是 A!多继承时各类的
super()串成一条链,每个类只执行一次。
四、魔术方法(Magic Methods)
也叫 dunder methods(double underscore,双下划线)。
NOTE 为什么叫「魔术」 你不需要显式调用它们,Python 在特定时机自动触发。比如写
+自动调__add__,print(obj)自动调__str__。 双下划线命名是 Python 官方保留的,不要自己发明新的__xxx__。
不定义会怎样
| 类型 | 不定义的结果 |
|---|---|
__str__ __eq__ __repr__ |
用 object 的默认实现,不报错 |
__add__ __len__ __getitem__ |
直接报 TypeError |
ABC 里的 @abstractmethod |
实例化时就报错 |
判断标准:object 基类里有没有默认实现。
分类速查
生命周期
| 方法 | 触发时机 | 说明 |
|---|---|---|
__new__ |
创建实例 | 比 __init__ 更早,单例/继承不可变类型时才用 |
__init__ |
创建实例 | 初始化属性,最常用 |
__del__ |
对象被回收 | 析构,时机不保证,慎用 |
字符串表示
| 方法 | 触发方式 | 受众 |
|---|---|---|
__str__ |
print() / str() |
给用户看 |
__repr__ |
repr() / 终端输入变量名 |
给开发者看 |
__format__ |
f"{obj:spec}" |
格式化语法 |
比较运算
| 方法 | 运算符 |
|---|---|
__eq__ __ne__ |
== != |
__lt__ __le__ |
< <= |
__gt__ __ge__ |
> >= |
TIP @functools.total_ordering 只需定义
__eq__和一个比较方法,其余四个自动推导。
数学运算
| 方法 | 运算符 | 反向版本 |
|---|---|---|
__add__ |
+ |
__radd__ |
__sub__ |
- |
__rsub__ |
__mul__ |
* |
__rmul__ |
__truediv__ |
/ |
__rtruediv__ |
EXAMPLE 反向方法(r 前缀)什么时候触发 左操作数不支持该运算时,Python 转去调右操作数的反向方法。
v * 3 # 调 v.__mul__(3) 3 * v # int 不认识 Vector → 转调 v.__rmul__(3)日常很少用,主要在写自定义数值类型(向量、矩阵、分数)或和 numpy 混用时才需要。
容器协议(让类像 list / dict)
| 方法 | 触发方式 |
|---|---|
__len__ |
len(obj) |
__getitem__ |
obj[key] |
__setitem__ |
obj[key] = v |
__contains__ |
x in obj |
__iter__ / __next__ |
for x in obj |
上下文管理(with 语句)
| 方法 | 触发时机 | 说明 |
|---|---|---|
__enter__ |
进入 with 块 | 返回值绑定到 as 变量 |
__exit__ |
离开 with 块 | 返回 True 可吞掉异常 |
其他实用
| 方法 | 触发方式 | 说明 |
|---|---|---|
__call__ |
obj() |
让实例像函数一样调用 |
__hash__ |
hash(obj) / 作字典 key |
⚠️ 定义 __eq__ 后默认变 None |
__bool__ |
if obj |
没有时用 __len__ 判断 |
DANGER ⚠️ eq 和 hash 联动 定义了
__eq__之后,__hash__会自动变成None,该类实例就不能放进set或当dict的 key。需要手动补回__hash__。
五、封装
命名约定(约定而非强制)
Python 没有 private 关键字,靠命名区分访问级别:
class BankAccount:
def __init__(self):
self.owner = "Alice" # 公开
self._balance = 1000 # 保护(约定别外部访问)
self.__secret = "key" # 私有(名称改写为 _BankAccount__secret)🆚 Java 有真正的 private,Python 的 __ 只是「名称改写」,仍能强行访问,只是不推荐。
@property 装饰器
用属性语法访问,同时加入校验逻辑(Java 的 getter/setter,但更干净)。
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property # getter
def celsius(self):
return self._celsius
@celsius.setter # setter
def celsius(self, value):
if value < -273.15:
raise ValueError("低于绝对零度")
self._celsius = value
t = Temperature(25)
t.celsius = 30 # 调用 setter
t.celsius # 调用 getterNOTE 多个属性 & 完整生命周期 每个属性单独写一套,
@xxx.setter里的xxx必须和@property的方法名一致。@property → 读 p.name @name.setter → 写 p.name = "x" @name.deleter → 删 del p.name
六、多态
🐤 鸭子类型
QUOTE 「如果它走路像鸭子、叫声像鸭子,那它就是鸭子。」
Python 不看类型,只看对象有没有这个方法。
def make_sound(obj): # 没有任何类型限制
obj.speak() # 只要有 speak() 就能跑
make_sound(Dog()) # 汪!
make_sound(Cat()) # 喵~
make_sound(Robot()) # beep boop(Robot 不是 Animal,照样能用)🆚 Java 必须声明 void makeSound(Animal animal),Python 完全不需要。
方法重写(override)
子类定义同名方法,覆盖父类的。Python 查找顺序:先找自己,再找父类。
class Animal:
def speak(self): return "..."
class Dog(Animal):
def speak(self): return "汪!" # 覆盖父类
Dog().speak() # "汪!"🆚 Python 没有方法重载(overload)
同名方法后面的会直接覆盖前面的,不能靠参数区分。
class Dog:
def speak(self): return "汪"
def speak(self, msg): return msg # ⚠️ 覆盖了上面那个用三种方式替代重载:
# 1. 默认参数(最常用)
def speak(self, msg="汪", times=1):
return msg * times
# 2. *args / **kwargs 接收任意参数
def speak(self, *args): ...
# 3. 类型判断(少用)
def speak(self, msg=None):
if isinstance(msg, int): ...SUMMARY 三个概念的关系
- 方法重写:怎么定义不同的行为
- 鸭子类型:怎么使用这些不同的行为
- 方法重载:Python 没有,用默认参数替代
七、抽象基类与接口
🆚 Python 没有 interface 关键字,用**抽象基类(ABC)**替代。
from abc import ABC, abstractmethod
class Shape(ABC): # 相当于 interface
@abstractmethod
def area(self): ... # 子类必须实现
def describe(self): # 普通方法,子类直接继承
return f"面积是 {self.area()}"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
# Shape() ← 报错:不能实例化含抽象方法的类NOTE 比 Java interface 更灵活 Java 的 interface 纯抽象,Python 的 ABC 可以混入普通方法——其实更像 Java 的抽象类。Python 把抽象类和接口合二为一了。
如果只是想调用某方法,靠鸭子类型即可,根本不需要 ABC。 想强制子类实现某些方法,才用 ABC。
abc 是标准库
abc 是 Python 标准库,装好 Python 就有,import abc 直接用,无需 pip。
| 类别 | 例子 | 是否需要安装 |
|---|---|---|
| 内置 | print len isinstance |
连 import 都不用 |
| 标准库 | abc os math collections functools |
import 即可,自带 |
| 第三方 | numpy pandas requests |
需要 pip install |
八、方法的三种类型
类里能定义三种方法,区别在于第一个参数和能访问什么。
class Demo:
data = "类属性"
def instance_method(self): # 普通方法:参数 self(实例)
return self.x, Demo.data # 能访问实例 + 类
@classmethod
def class_method(cls): # 类方法:参数 cls(类)
return cls.data # 只能访问类
@staticmethod
def static_method(a, b): # 静态方法:无 self / cls
return a + b # 谁都访问不了,纯计算| 装饰器 | 第一个参数 | 能访问 | 典型用途 | |
|---|---|---|---|---|
| 普通方法 | 无 | self(实例) |
实例属性 + 类属性 | 操作具体对象 |
| 类方法 | @classmethod |
cls(类) |
仅类属性 | 工厂方法 |
| 静态方法 | @staticmethod |
无 | 都不能 | 工具/校验/转换函数 |
TIP 怎么选——一句话决策
- 用到了
self(实例数据)→ 普通方法- 用到了
cls(类属性、或要造对象)→ 类方法- 两个都没用到 → 静态方法(甚至可考虑写成模块级普通函数)
WARNING ⚠️ 用类名调实例方法,self 要自己传 实例方法本质是「类里的普通函数」,只有用对象调时 Python 才自动把对象塞给
self:class Student: def say(self): print("hello") s = Student() s.say() # ✅ 等价于 Student.say(s),自动传 s Student.say(s) # ✅ 手动传对象,也行 Student.say() # ❌ TypeError: missing 1 required positional argument: 'self'带参数时第一个位置依然是 self:
Student.introduce(s, "张三")才对;写Student.introduce("张三")会把"张三"当成 self,然后报name缺参。 类方法 / 静态方法没这问题——cls由 Python 自动给,静态方法压根不需要。
类方法 @classmethod
第一个参数 cls 指向类本身,不是实例。可用类名直接调,不需要创建对象。
class Dog:
count = 0
@classmethod
def class_method(cls):
return cls.count # 访问类属性
Dog.class_method() # ✅ 直接用类名调用工厂方法(类方法最常见的用途)
NOTE 什么是工厂方法 一个专门用来「生产」对象的方法——你不直接构造,而是调一个方法帮你造。 因为 Python 只能有一个
__init__(没有重载),工厂方法用来补足「多种创建方式」。
class Date:
def __init__(self, y, m, d): # 唯一的构造函数
self.y, self.m, self.d = y, m, d
@classmethod
def from_string(cls, s): # 工厂①:从字符串造
y, m, d = map(int, s.split("-"))
return cls(y, m, d) # cls(...) 等价于 Date(...)
@classmethod
def today(cls): # 工厂②:造今天
import datetime
t = datetime.date.today()
return cls(t.year, t.month, t.day)
# 多个「构造入口」
d1 = Date(2025, 6, 14) # 常规构造
d2 = Date.from_string("2025-06-14") # 工厂方法
d3 = Date.today() # 工厂方法WARNING ⚠️ 工厂里要用
cls(...)而不是写死Date(...)cls会自动指向实际调用的类,继承时才不出错。class ChineseDate(Date): pass d = ChineseDate.from_string("2025-06-14") type(d) # ChineseDate ✅(写死 Date(...) 则会返回 Date ❌)
EXAMPLE 标准库里早就用过的工厂方法
dict.fromkeys(["a", "b"], 0) # dict 的工厂方法 int.from_bytes(b"\x00\x10") # int 的工厂方法 datetime.now() # datetime 的工厂方法命名习惯:
from_xxx(从某种数据造)或create_xxx。
静态方法 @staticmethod
没有 self 也没有 cls,本质就是「恰好放在类里的普通函数」。
用途:逻辑上属于这个类、但不依赖任何实例或类状态的工具函数。
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@staticmethod
def c_to_f(c): # 纯转换,不需要任何实例数据
return c * 9 / 5 + 32
@staticmethod
def is_freezing(celsius): # 纯判断
return celsius <= 0
Temperature.c_to_f(25) # 77.0,不用创建对象就能调TIP 静态方法的意义在于「归类」 把相关逻辑收进类里,比散落在外面更清晰。但如果一个静态方法和这个类毫无关系,那它更适合直接放在模块里当普通函数。
九、Python vs Java 速查表
| 概念 | Java | Python |
|---|---|---|
| 静态变量 | static 字段 |
类属性(⚠️ 实例赋值有陷阱) |
| 私有成员 | private(强制) |
_ / __(约定) |
| getter/setter | 手写方法 | @property |
| 调用父类构造 | 自动插入 super() |
手动写 super().__init__() |
| 单继承 / 多继承 | 仅单继承 | 支持多继承(注意 MRO) |
| 方法重写 | ✅ override | ✅ override |
| 方法重载 | ✅ overload | ❌ 用默认参数替代 |
| 多态 | 需声明类型 | 🐤 鸭子类型,不看类型 |
| 接口 | interface |
抽象基类 ABC |
| 反射 | Reflection API | getattr / setattr |
| 多种构造函数 | 构造函数重载 | @classmethod 工厂方法 |
| 静态方法 | static 方法 |
@staticmethod(无 self/cls) |
SUCCESS 复习重点
- 类属性赋值陷阱:实例赋值会新建实例属性遮住类属性,必须用类名修改
- 子类有
__init__必须手动调super();没调时只丢父类实例属性,方法/类属性/类方法/静态方法照常继承super()沿 MRO 走,不是简单的父类__eq__和__hash__的联动- Python 没有重载,用默认参数
- 鸭子类型 + ABC 替代 Java 的接口
- 三种方法的区别:
self(实例) /cls(类) / 无(静态);用类名调实例方法要手动传 self- 工厂方法用
@classmethod+cls(...),弥补单一__init__- 类属性继承是查找而非复制:可变类属性原地改会串到父类;想改父类类属性用
父类名.属性,类方法里别写cls.属性